Skip to content

feat(share): flat end-of-conversation band + split copy-link button - #6369

Closed
tsahimatsliah wants to merge 9 commits into
claude/share-end-of-conversationfrom
claude/share-split-copy-button
Closed

feat(share): flat end-of-conversation band + split copy-link button#6369
tsahimatsliah wants to merge 9 commits into
claude/share-end-of-conversationfrom
claude/share-split-copy-button

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Jul 23, 2026

Copy link
Copy Markdown
Member

Stacked on #6349 (claude/share-end-of-conversation), which is itself stacked on #6343. Review #6343#6349 → this one, or read this diff alone — it only touches the band and its share control.

#6349 introduced the end-of-conversation share band as a bordered card with an icon-only share trigger, behind two feature flags. This PR is the design pass on top of it: the band goes flat by default, the trigger becomes a labelled split button, and the band ships to everyone.

Shipping to everyone

The band is no longer gated. share_end_of_conversation is deleted (nothing else read it) and the sharing_visibility kill-switch check is dropped from this component, so the comment threshold is the only condition. The flag and useSharingVisibility stay in the codebase — they gate the other surfaces in the initiative.

That means no runtime off-switch for this band: turning it off after merge takes a revert. Flagged deliberately, per the product call to show it to everyone.

The band

flat is now the default variant: no fill, no card border, just a top hairline separating the strip from the comments above it. card is still available via variant="card" for surfaces that want the heavier self-contained block.

The split copy control

ShareActions gains a split variant — two real buttons that read as one control:

  • Left half copies the link straight away. The copy glyph cross-fades into a green check on success (opacity + scale + blur, all compositor-only, collapsing to an instant swap under prefers-reduced-motion).
  • Right half is a chevron that opens the design-system DropdownMenu with the standard social tiles. It rotates to point back at the menu it opened.

Geometry follows the button guidelines exactly rather than approximating them: at every ButtonSize the control matches a standard button's height, corner radius, type scale and outer padding. The only deviations are at the shared edge — both inner paddings tighten one step so the seam isn't sitting in a canyon, and the chevron drops the icon-only square so it hugs the seam symmetrically.

The divider

The two halves are separated by a single rule, not two borders meeting:

Variant Divider
Secondary / Float / Subtle the chevron half's own DS border (the main half drops border-r) — full height, matching the outer edge in width, colour and hover transition by construction
Primary no border to inherit, so a full-height rule in the label colour at 20%
Tertiary a ghost button, so a full-height rule would float — a shorter rule (6px inset) in exactly the colour tailwind/buttons.ts gives Subtle's border

Two build-level gotchas worth knowing, both commented in the code:

  • bg-current compiles to nothing here — this theme replaces Tailwind's colors wholesale and has no current key. A hairline drawn with it renders as no hairline, which is what let the two real borders read as a double rule in the first place.
  • before:opacity-* does not survive this project's Tailwind build on pseudo-elements: the class lands in the DOM and the element renders at full strength. Both dividers mix their alpha into the colour instead.

Structure

  • SplitShareButton.tsx — the control (padding maps, divider, chevron, dropdown).
  • CopyStateIcon.tsx — the cross-fading copy/check glyph, shared with the mobile and popover paths.
  • ShareActions.tsx — back to a variant router.

The icon variant still uses the existing Popover rather than DropdownMenu; migrating it would change the DOM of every icon-only share surface in the app, so it is deliberately left for a follow-up.

Storybook

  • Components/Share/EndOfConversationShare — every state (below/at/above threshold, no comments, flat vs card, dark/light), plus a real 390px iframe for the mobile path.
  • Components/Share/SplitShareButton — the split control across all five variants and all five sizes, and an AgainstStandardButtons page that prints live computed geometry (height, radius, type, padding) beside the standard buttons so the guideline match is checkable rather than asserted.

Verification

  • 18 tests pass across ShareActions.spec.tsx, EndOfConversationShare.spec.tsx, PostComments.spec.tsx, including a new test that opens the dropdown and asserts the social tiles are present. Tests for the now-removed flag paths are deleted rather than inverted.
  • packages/webapp PostPage suite passes (48 tests) — cross-package check per AGENTS.md.
  • Shared lint clean, changed-file strict typecheck clean.
  • Divider colours verified in-browser against Subtle's computed borderLeftColor.

Seeing it on the preview

Open any post with more than 3 comments — no flags to toggle.

🤖 Generated with Claude Code

Preview domain

https://claude-share-split-copy-button.preview.app.daily.dev

@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daily-webapp Error Error Jul 28, 2026 12:39pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
storybook Ignored Ignored Jul 28, 2026 12:39pm

Request Review

tsahimatsliah and others added 3 commits July 23, 2026 15:19
Adds a `split` variant to ShareActions: two real buttons that read as one
control. The left half copies the link and cross-fades its glyph to a green
check; the right half is a chevron that opens the standard DropdownMenu with
the social tiles.

Geometry matches a standard Button at every size — height, radius, type scale
and outer padding — deviating only at the shared edge, where both inner
paddings tighten one step and the chevron drops its icon-only square.

The divider is a single rule: the chevron half keeps its own DS border while
the main half drops `border-r`, since two borders meeting is what reads as a
double line. Borderless variants have no border to inherit and draw their own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drops the card's fill and border for a single hairline rule separating the
strip from the comments above it, and moves the trigger to the labelled split
button. `variant="card"` keeps the heavier self-contained surface for surfaces
that want it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every band state (either side of the threshold, flat vs card, in context below
a comment list, a real 390px mobile frame) and the split control across all
variants and sizes, with computed geometry printed beside the standard buttons
so the guideline match is checkable rather than asserted.

The auth/log/query decorator the share stories share is extracted to
`share.mocks.tsx` — it had been copied verbatim into three story files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes both gates: the `share_end_of_conversation` experiment flag (deleted,
nothing else read it) and the `sharing_visibility` kill-switch check. The
comment threshold is now the only condition, so the two-component split
(gated wrapper + ungated band) collapses into one.

`useSharingVisibility` and `sharing_visibility` stay — they gate the other
surfaces in the initiative, this one just no longer consumes them.

Tests for the discarded path are removed rather than inverted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review pass over the PR:

- The copy confirmation was swapped in for every ShareActions variant, which
  silently restyled the icon-only triggers on the feed card, brief header and
  mobile share widget. Those keep the existing `secondary` fill; the green
  check is now the split control's alone.
- The flat band set `py-4 pt-6`, which resolved correctly only because
  Tailwind emits `pt-*` after `py-*` in the stylesheet — class order in the
  attribute has no bearing on it. Now `pb-4 pt-6`.
- `hasActiveDiscussion` lost its last external caller with the flag wrapper,
  so it is no longer exported.
- Comment still cited the old "Share this discussion" label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The chevron carried `pressed`, which renders `aria-pressed` and is painted
  as a held-down fill. Removed: the rotation is the only open-state signal the
  control wants, and Radix already sets `aria-expanded`, which is the correct
  semantic for a menu trigger.
- The split dropdown repeated a "Share" heading under a trigger that already
  says so, and centred its tiles. Heading dropped and tiles aligned left. The
  popover used by the icon-only surfaces keeps both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `p-4` on DropdownMenuContent never applied — the dropdown's CSS module
applies `p-1.5` at the same specificity and wins on stylesheet order, leaving
6px. And `flex-wrap` with `justify-start` collected all the leftover row width
on one side, so the insets measured top 7 / right 41 / bottom 29 / left 15.

Now `!p-4` so the override lands, and a 4-column grid whose columns divide the
content box exactly. Measured insets: top 17 / right 17.8 / bottom 17 /
left 17.8 — the 1.8px is cell-centring slack, identical on both sides.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tsahimatsliah added a commit that referenced this pull request Jul 27, 2026
Share moves out of the buried "..." options menu into the header action row on
both entity pages, as PR #6369's SplitShareButton: the label copies the link and
the chevron drops the standard social list.

- New `EntityShareAction` wraps `ShareActions` with the entity's link/text/cid
  and logs `ShareTag`/`ShareSource` with the provider and origin. `display`
  defaults to the split control; `icon` keeps the icon-only trigger.
- Block leaves the row for the "..." menu, so the row is one Follow button plus
  identical secondary controls. Blocked is the exception: Unblock stays in the
  row, in the Follow slot that is empty then, and leaves the menu.
- The notification bell takes ButtonVariant.Float so the bell, the copy control
  and the "..." button read as one treatment.
- `CustomFeedOptionsMenu` gains `hideShare` so the in-menu Share entry is
  dropped where the visible control renders, never both.
- `SourceActions` gains `showShare`; only /sources/[source] passes it, so the
  post page highlights widget keeps its existing row.

No feature flag: this ships to everyone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tsahimatsliah added a commit that referenced this pull request Jul 27, 2026
Share moves out of the buried "..." options menu into the header action row on
both entity pages, as PR #6369's SplitShareButton: the label copies the link and
the chevron drops the standard social list.

- New `EntityShareAction` wraps `ShareActions` with the entity's link/text/cid
  and logs `ShareTag`/`ShareSource` with the provider and origin. `display`
  defaults to the split control; `icon` keeps the icon-only trigger.
- Block leaves the row for the "..." menu, so the row is one Follow button plus
  identical secondary controls. Blocked is the exception: Unblock stays in the
  row, in the Follow slot that is empty then, and leaves the menu.
- The notification bell takes ButtonVariant.Float so the bell, the copy control
  and the "..." button read as one treatment.
- `CustomFeedOptionsMenu` gains `hideShare` so the in-menu Share entry is
  dropped where the visible control renders, never both.
- `SourceActions` gains `showShare`; only /sources/[source] passes it, so the
  post page highlights widget keeps its existing row.

No feature flag: this ships to everyone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tsahimatsliah added a commit that referenced this pull request Jul 27, 2026
Share moves out of the buried "..." options menu into the header action row on
both entity pages, as PR #6369's SplitShareButton: the label copies the link and
the chevron drops the standard social list.

- New `EntityShareAction` wraps `ShareActions` with the entity's link/text/cid
  and logs `ShareTag`/`ShareSource` with the provider and origin. `display`
  defaults to the split control; `icon` keeps the icon-only trigger.
- Block leaves the row for the "..." menu, so the row is one Follow button plus
  identical secondary controls. Blocked is the exception: Unblock stays in the
  row, in the Follow slot that is empty then, and leaves the menu.
- The notification bell takes ButtonVariant.Float so the bell, the copy control
  and the "..." button read as one treatment.
- `CustomFeedOptionsMenu` gains `hideShare` so the in-menu Share entry is
  dropped where the visible control renders, never both.
- `SourceActions` gains `showShare`; only /sources/[source] passes it, so the
  post page highlights widget keeps its existing row.

No feature flag: this ships to everyone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The band's sharing tests covered copy (main half + dropdown tile) and native,
but not the individual social networks. Adds a parametrised case for X,
WhatsApp and LinkedIn asserting each logs LogEvent.SharePost with its own
ShareProvider and Origin.EndOfConversation, opens a window, and never touches
the clipboard — so a network share and a copy can't conflate in the funnel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tomeredlich added a commit that referenced this pull request Sep 1, 2026
"Tsahi's button" is SplitShareButton from #6369 — copy link and a chevron
holding the networks, meeting at one hairline — wrapped by ShareBand, which
pairs it with a title and a line of copy. #6369 built it for exactly these two
surfaces: the band below an active discussion and the prompt after an upvote.

Neither PR landed. #6369 is closed and #6378 merged into it, so the components
exist on no branch that ships. They are carried here — ShareBand, ShareActions,
SplitShareButton, CopyStateIcon and the Origin they log against — and both
prompts now render through ShareBand rather than the pair of hand-rolled rows
I had built. If that stack revives, these two callers should collapse into its
EndOfConversationShare and the copies here should go.

The copy is theirs too, so the two bands read as one pair.

Testing: shared 382 suites / 2680 tests, webapp 81 / 644, extension 6 / 52,
package lint clean for shared and webapp. The ported ShareActions spec runs
green here as it does there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich added a commit that referenced this pull request Sep 2, 2026
Three controls inside the brief itself:

- The post page's highlight bar (#6556), scoped to a body container so a
  selection in the header or the preferences card never raises it.
- A copy icon per bullet and paragraph, carrying the text and the brief
  link, after #6350's copy-summary icon. Each one is labelled with what
  it copies, since a brief carries a column of them.
- A Snapshot on the Must know heading that captures its bullets as one
  card, with the claim before the colon as each line.

None of these can be declared in JSX: the body is a single `<Markdown
content={contentHtml} />`, so briefBodyBlocks reads the blocks back out
of the rendered DOM — which is also the most faithful source, being what
the reader is looking at — and the controls are portalled into them.
Hosts are marked and swept before each run, because Markdown re-renders
on its own for hover cards and the image modal.

The closing band is #6369's ShareBand with the briefing's copy, so it
cannot drift from the end-of-conversation band it was asked to match.

All of it sits behind `briefing_share_controls`, default false.

Mockup-to-eng-pass: 1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich added a commit that referenced this pull request Sep 3, 2026
…o choose from

The Snapshot button becomes a Share button: the icon and label are the ones
people already recognise, and pressing it opens a menu that previews the
designed 1080² card before offering it.

The menu opens immediately with a skeleton rather than waiting on the render.
Rasterizing takes a second or two, and a control that does nothing visible for
that long reads as broken — so the capture is deferred a tick, letting the menu
paint first, and Copy link stays usable while the card is still rendering.

Seven layouts are up for review in Features/Snapshot/Menu styles, all live
against the same card: rows, rows-centred, an overlay pill on the card, a
compact no-preview variant, Tsahi's split button from #6369, and the two ways
the image can sit in the share modal — its own section or a single tile. The
`menuVariant` prop exists to serve that page and comes out with the losers.

The image is one action, not a mode over the sheet's other targets. A link needs
a destination, which is why there are twenty of them; an image does not, because
the OS share sheet and the clipboard are the destinations, and no web share
intent takes an image anyway. So the image owns its own action and the squads
and external tiles keep sharing the link — which is what keeps this on one
screen instead of two.

Pressing it hands the PNG to the native sheet where the platform takes files,
copies it to the clipboard otherwise (with a toast, since the clipboard has no
UI of its own), and downloads it only as a last resort.

Capture moves into `useSnapshotCapture` so the dropdown and the modal section
render from one implementation. Its `isActive` gate matters twice: a feed never
carries a 1080px card per item until someone asks to share, and because the
modal pre-renders on open, the click handler reaches `clipboard.write` with no
await in front of it — still inside the user gesture the clipboard requires.

Two surfaces had no card at all, from the #6579 and #6581 reviews:

- `FeedSnapshotCard` carries a shared feed's recipe — tags and source count —
  rather than its posts, which are a standing filter and would go stale.
- `AwardSnapshotCard` names the sender. It is the only status moment that comes
  from someone else, and a gift with no giver reads as self-congratulation.

Tsahi's `SplitShareButton` and `CopyStateIcon` come across from #6369 unchanged
apart from four additive props — `onOpenChange`, `header`, `menuLayout` and a
width override — so his grid is untouched when nobody passes them.

`SocialShareList`, `SocialShareButton` and `SocialShareContainer` gain optional
size and compact props, defaulting to today's values, so the dropdown can
tighten without moving anything else in the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich added a commit that referenced this pull request Sep 3, 2026
Three controls inside the brief itself:

- The post page's highlight bar (#6556), scoped to a body container so a
  selection in the header or the preferences card never raises it.
- A copy icon per bullet and paragraph, carrying the text and the brief
  link, after #6350's copy-summary icon. Each one is labelled with what
  it copies, since a brief carries a column of them.
- A Snapshot on the Must know heading that captures its bullets as one
  card, with the claim before the colon as each line.

None of these can be declared in JSX: the body is a single `<Markdown
content={contentHtml} />`, so briefBodyBlocks reads the blocks back out
of the rendered DOM — which is also the most faithful source, being what
the reader is looking at — and the controls are portalled into them.
Hosts are marked and swept before each run, because Markdown re-renders
on its own for hover cards and the image modal.

The closing band is #6369's ShareBand with the briefing's copy, so it
cannot drift from the end-of-conversation band it was asked to match.

All of it sits behind `briefing_share_controls`, default false.

Mockup-to-eng-pass: 1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich added a commit that referenced this pull request Sep 3, 2026
Three controls inside the brief itself:

- The post page's highlight bar (#6556), scoped to a body container so a
  selection in the header or the preferences card never raises it.
- A copy icon per bullet and paragraph, carrying the text and the brief
  link, after #6350's copy-summary icon. Each one is labelled with what
  it copies, since a brief carries a column of them.
- A Snapshot on the Must know heading that captures its bullets as one
  card, with the claim before the colon as each line.

None of these can be declared in JSX: the body is a single `<Markdown
content={contentHtml} />`, so briefBodyBlocks reads the blocks back out
of the rendered DOM — which is also the most faithful source, being what
the reader is looking at — and the controls are portalled into them.
Hosts are marked and swept before each run, because Markdown re-renders
on its own for hover cards and the image modal.

The closing band is #6369's ShareBand with the briefing's copy, so it
cannot drift from the end-of-conversation band it was asked to match.

All of it sits behind `briefing_share_controls`, default false.

Mockup-to-eng-pass: 1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich added a commit that referenced this pull request Sep 6, 2026
Three controls inside the brief itself:

- The post page's highlight bar (#6556), scoped to a body container so a
  selection in the header or the preferences card never raises it.
- A copy icon per bullet and paragraph, carrying the text and the brief
  link, after #6350's copy-summary icon. Each one is labelled with what
  it copies, since a brief carries a column of them.
- A Snapshot on the Must know heading that captures its bullets as one
  card, with the claim before the colon as each line.

None of these can be declared in JSX: the body is a single `<Markdown
content={contentHtml} />`, so briefBodyBlocks reads the blocks back out
of the rendered DOM — which is also the most faithful source, being what
the reader is looking at — and the controls are portalled into them.
Hosts are marked and swept before each run, because Markdown re-renders
on its own for hover cards and the image modal.

The closing band is #6369's ShareBand with the briefing's copy, so it
cannot drift from the end-of-conversation band it was asked to match.

All of it sits behind `briefing_share_controls`, default false.

Mockup-to-eng-pass: 1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich added a commit that referenced this pull request Sep 6, 2026
…placement

The seven menu layouts were there to pick one, and the buttons they were
picking between are not new — Share and Copy link already exist in the
app. What this PR is actually about is generating the card and deciding
where the control sits, so the exploration comes out:

- Features/Snapshot/Menu styles, and the menuVariant prop behind it
- SnapshotImageSection, which only the modal candidates rendered
- SplitShareButton and CopyStateIcon, which belong to #6369
- the optional size and compact props on SocialShareList,
  SocialShareButton and SocialShareContainer, added only for those
  candidates

SnapshotButton is a plain button again: press it and it shares the card.
Rendering starts on hover or focus rather than on mount, so a feed does
not carry a 1080px card per item — and because the render is already
done by the time the press lands, the clipboard still has the gesture it
needs. A press that arrives first waits for the render and downloads
instead, which is the one path the clipboard cannot take.

Two type fixes on SocialShareButton stay: the size map is indexed with
the full ButtonSize union, and the ref is passed to a Button that renders
as an anchor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant